package com.googlecode.yoohoo.xmppcore.protocol.stream;

import java.io.StringReader;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import com.googlecode.yoohoo.xmppcore.protocol.InvalidJabberIdException;
import com.googlecode.yoohoo.xmppcore.protocol.JabberId;
import com.googlecode.yoohoo.xmppcore.protocol.XmppProtocolException;
import com.googlecode.yoohoo.xmppcore.protocol.parsing.IParser;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.BadFormat;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.BadNamespacePrefix;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.InvalidNamespace;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.InvalidXml;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.RestrictedXml;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.XmlNotWellFormed;
import com.googlecode.yoohoo.xmppcore.protocol.translation.ITranslator;
import com.googlecode.yoohoo.xmppcore.protocol.translation.IXmlWriter;
import com.googlecode.yoohoo.xmppcore.protocol.translation.XmlWriter;
import com.googlecode.yoohoo.xmppcore.utils.ErrorUtils;

public class Stream implements IParser, ITranslator {
	private static final String DEFAULT_PREFIX = "";
	public static final String NAMESPACE_URI_STREAM = "http://etherx.jabber.org/streams";
	public static final String ATTRIBUTE_NAME_FROM = "from";
	public static final String ATTRIBUTE_NAME_TO = "to";
	public static final String ATTRIBUTE_NAME_ID = "id";
	public static final String ATTRIBUTE_NAME_VERSION = "version";
	public static final String PREFIX_NAME_XML = "xml";
	public static final String ATTRIBUTE_NAME_LANG = "lang";
	public static final String ELEMENT_NAME_STREAM = "stream";
	public static final String PREFIX_NAME_STREAM = "stream";
	public static final String NAMESPACE_PREFIX_STREAM = "stream";
	public static final String NAMESPACE_URI_CLIENT_TO_SERVER = "jabber:client";
	public static final String NAMESPACE_URI_SERVER_TO_SERVER = "jabber:server";
	public static final String DEFAULT_XML_LANG = "en_US";
	public static final String DEFAULT_VERSION = "1.0";
	
	private JabberId from;
	private JabberId to;
	private String id;
	private String version;
	private StreamType streamType;
	private String xmlLang;
	
	public Stream() {
		streamType = StreamType.CLIENT_TO_SERVER;
	}
	
	public JabberId getFrom() {
		return from;
	}
	
	public JabberId getTo() {
		return to;
	}
	
	public String getId() {
		return id;
	}
	
	public String getVersion() {
		return version;
	}
	
	public void setFrom(JabberId from) {
		this.from = from;
	}
	
	public void setTo(JabberId to) {
		this.to = to;
	}
	
	public void setId(String id) {
		this.id = id;
	}
	
	public void setVersion(String version) {
		this.version = version;
	}
	
	public void setStreamType(StreamType streamType) {
		if (streamType == null)
			throw new IllegalArgumentException("Null stream type.");
		this.streamType = streamType;
	}
	
	public StreamType getStreamType() {
		return streamType;
	}

	@Override
	public String toXml() {
		IXmlWriter writer = new XmlWriter();
		writer.writeStartElement(PREFIX_NAME_STREAM, ELEMENT_NAME_STREAM);
		
		if (id != null) {
			writer.writeAttribute(ATTRIBUTE_NAME_ID, id);
		}
		
		if (from != null) {
			writer.writeAttribute(ATTRIBUTE_NAME_FROM, from.toString());
		}
		
		if (to != null) {
			writer.writeAttribute(ATTRIBUTE_NAME_TO, to.toString());
		}
		
		if (StreamType.CLIENT_TO_SERVER == streamType) {
			writer.writeNamespace(NAMESPACE_URI_CLIENT_TO_SERVER);
		} else {
			writer.writeNamespace(NAMESPACE_URI_SERVER_TO_SERVER);
		}
		
		writer.writePrefix(PREFIX_NAME_STREAM, NAMESPACE_URI_STREAM);
		
		if (xmlLang != null) {
			writer.writeAttribute(PREFIX_NAME_XML, ATTRIBUTE_NAME_LANG, xmlLang);
		}
		
		if (version != null) {
			writer.writeAttribute(ATTRIBUTE_NAME_VERSION, version);
		} else {
			writer.writeAttribute(ATTRIBUTE_NAME_VERSION, DEFAULT_VERSION);
		}
		
		writer.writeCharacters(null); // this line make XmlWriter writing element close tag('>').
		
		return writer.toXml();		
	}

	@Override
	public void fromXml(String xml) {
		XMLInputFactory factory = XMLInputFactory.newInstance();
		XMLStreamReader reader = null;
		try {
			reader = factory.createXMLStreamReader(new StringReader(xml));
			int eventType = reader.next();
			
			if (eventType != XMLStreamConstants.START_ELEMENT) {
				throw new XmppProtocolException(new RestrictedXml());
			}
			
			// must be stream tag
			if (!ELEMENT_NAME_STREAM.equals(reader.getLocalName()))
				throw new XmppProtocolException(new InvalidXml());
			
			// must be stream:steam tag
			if (!NAMESPACE_PREFIX_STREAM.equals(reader.getPrefix()))
				throw new XmppProtocolException(new BadNamespacePrefix());
			
			// (rfc3920 11.2)
			// check namespaces
			// two or three namespaces: 'jabber:client', 'stream', xml(optional)
			if (reader.getNamespaceCount() != 2 && reader.getNamespaceCount() != 3)
				throw ErrorUtils.createProtocolException("Too much or too less namespace count", new BadNamespacePrefix());
			
			for (int i = 0; i < reader.getNamespaceCount(); i++) {
				String namespacePrefix = reader.getNamespacePrefix(i);
				if (namespacePrefix == null) { // default namespace(rfc3920 11.2.2)
					// default namespace uri must be 'jabber:client' or 'jabber:server'
					String defaultNamespace = reader.getNamespaceURI(i);
					if (NAMESPACE_URI_CLIENT_TO_SERVER.equals(defaultNamespace)) {
						streamType = StreamType.CLIENT_TO_SERVER;
					} else if (NAMESPACE_URI_SERVER_TO_SERVER.equals(reader.getNamespaceURI(i))) {
						streamType = StreamType.SERVER_TO_SERVER;
					} else {
						throw ErrorUtils.createProtocolException("Bad default namespace URI. Must be 'jabber:client' or 'jabber:server'.", new BadNamespacePrefix());
					}
				} else if (NAMESPACE_PREFIX_STREAM.equals(namespacePrefix)) { // stream namespace(rfc3920 11.2.1)
					// namespace uri for prefix 'stream' must be 'http://etherx.jabber.org/streams'
					if (!NAMESPACE_URI_STREAM.equals(reader.getNamespaceURI(i)))
						throw ErrorUtils.createProtocolException("Invalid namespace URI for prefix 'stream'.", new InvalidNamespace());
				} else { // xml namespace
					if (!PREFIX_NAME_XML.equals(namespacePrefix))
						throw ErrorUtils.createProtocolException(String.format("Namespace prefix '%s' not allowed here.",
								namespacePrefix), new BadNamespacePrefix());
				}
			}
			
			// (rfc3920 4.4)
			// check attributes
			for (int i = 0; i < reader.getAttributeCount(); i++) {
				String prefix = reader.getAttributePrefix(i);
				String attributeName = reader.getAttributeLocalName(i);
				
				if (DEFAULT_PREFIX.equals(prefix)) { // default namespace
					// jid only include one field: domain
					if (ATTRIBUTE_NAME_TO.equals(attributeName)) {
						try {
							to = JabberId.parseJidString(reader.getAttributeValue(i));
						} catch (InvalidJabberIdException e) {
							throw ErrorUtils.createProtocolException("Attribute 'to' isn't a valid jabber id.", new InvalidXml());
						}
					} else if (ATTRIBUTE_NAME_FROM.equals(attributeName)) {
						try {
							from = JabberId.parseJidString(reader.getAttributeValue(i));
						} catch (InvalidJabberIdException e) {
							throw ErrorUtils.createProtocolException("Attribute 'from' isn't a valid jabber id.", new InvalidXml());
						}
					} else if (ATTRIBUTE_NAME_VERSION.equals(attributeName)) {
						version = reader.getAttributeValue(i);
					} else if (ATTRIBUTE_NAME_ID.equals(attributeName)) {
						id = reader.getAttributeValue(i);
					} else {
						throw ErrorUtils.createProtocolException(String.format("Invalid attribute '%s'.", attributeName), new InvalidXml());
					}
				} else { // xml namespace
					// xml:lang
					if (!ATTRIBUTE_NAME_LANG.equals(attributeName))
						throw ErrorUtils.createProtocolException(String.format("Invalid attribute 'xml:%s'.", attributeName), new InvalidXml());
					
					xmlLang = reader.getAttributeValue(i);
				}
			}
			
			if (reader.getLocation().getCharacterOffset() != xml.length()) {
				throw new XmppProtocolException(new BadFormat());
			}
		} catch (XMLStreamException e) {
			throw new XmppProtocolException(new XmlNotWellFormed());
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (XMLStreamException e) {
					// ignore
				}
			}
		}
		
		if (to == null)
			throw new XmppProtocolException(new InvalidXml());
		
		if (version == null)
			throw new XmppProtocolException(new InvalidXml());

		// set default xml:lang
		if (xmlLang == null) {
			xmlLang = DEFAULT_XML_LANG;
		}
	}
	
}
